import java.net.*;
import java.io.*;
import java.util.HashMap;

/*
 * A simple SIP answering machine (AM)
 */
public class SIPConnecter {

	//datagram socket to use
	DatagramSocket ds;
	Property properties;
	NetworkInterface nif;
	HashMap<String,Integer> callTags = new HashMap<String,Integer>(); 
	HashMap<String,String> callBranch = new HashMap<String,String>(); 
	Integer tagCounter = 0;

	public SIPConnecter(String[] args) {
		//save properties from file
		properties = new Property(args);
		//get user name for the AM user
		System.out.println(properties.getProperty("sip_user"));
		
		//set up datagram socket
		try {
			System.out.println(properties.getProperty("sip_port"));
			ds = new DatagramSocket(Integer.parseInt(properties.getProperty("sip_port").trim()), InetAddress.getByName(properties.getProperty("sip_interface").trim()));
		}catch(SocketException e){
			error(e.toString(), true);
		}catch(UnknownHostException e){
			error(e.toString(), true);
		}

	}

	//listen for incoming packets
	public void listen() {
		while(true){
			SIPparser sp = new SIPparser();
			SIPparser spErrorChecker = new SIPparser(); //contains SIP-data from packets following the first INVITE
			//make packet-buffers
			byte[] buf = new byte[1024];
			byte[] bufTmp = new byte[1024];
			//structures to store packets
			DatagramPacket invitePacket = new DatagramPacket(buf, buf.length);
			DatagramPacket generalPacket = new DatagramPacket(bufTmp, bufTmp.length);
			try{
				ds.receive(invitePacket); //receive the initial invite packet from the caller
				String data = new String(invitePacket.getData()); //store its data
				//split the packet data by the SIP Header ending (get 2 parts with SIP and SDP)
				String[] splittedData = data.split("\\r?\\n\\r?\\n");
				String error = sp.parse(splittedData[0]); //parse the SIP data
				callTags.put(sp.callId, tagCounter++);
				callBranch.put(sp.callId, "z9hG4bK" + System.currentTimeMillis()+sp.callId); //put a new branch in the map
				System.out.println(sp.type.split(" ")[0]);
				if(sp.type.split(" ")[0].equals("INVITE")){ //if INVITE header
					if(!sp.user.equals(properties.getProperty("sip_user").trim())){ //retrieve the designated recipient in from field 
						sendErr(sp,invitePacket,"404 Not Found"); //it didn't match the user of the AM
						continue;
					}
					trying(sp, invitePacket); //send Trying-packet so that the caller can stop sending INVITEs
					try{
						Thread.sleep(2000); //delay to let the caller play keypad sound 
						ringing(sp, invitePacket);
						Thread.sleep(5000); //delay to let the caller play connection sound
						sendOk(sp, splittedData[1], invitePacket, "INVITE"); //send OK for the INVITE transaction
						boolean done = false;
						long time = System.currentTimeMillis()/1000;
						while(!done){
							//build packet structure for recving packets
							bufTmp = new byte[1024];
							generalPacket = new DatagramPacket(bufTmp, bufTmp.length); 
							ds.receive(generalPacket);
							spErrorChecker.parse(new String(bufTmp)); //parse SIP data
							debug("CALLID: "+spErrorChecker.callId + "; TYPE :" +spErrorChecker.type.split(" ")[0].trim());
							//check if we got an ACK for our OK packet, with the current session
							if(spErrorChecker.callId.trim().equals(sp.callId.trim()) && spErrorChecker.type.split(" ")[0].trim().equals("ACK")){
								break; 
								//gotCorrectPacket = true;
							//we got a CANCEL packet, OK it and terminate the session gracefully by sending Request Terminated
							}else if(spErrorChecker.callId.trim().equals(sp.callId.trim()) && spErrorChecker.type.split(" ")[0].trim().equals("CANCEL")){ 
								sendOk(sp, splittedData[1], invitePacket, "CANCEL");
								terminate(sp, invitePacket);
								done = true;
								callTags.remove(sp.callId);
								callBranch.remove(sp.callId);
								break;
							//if someone tries to call us when we are in a call, send them a BUSY-packet
							}else if(spErrorChecker.type.split(" ")[0].trim().equals("INVITE")){
								busy(spErrorChecker, generalPacket);
							}else if(System.currentTimeMillis()/1000 - time > 5){
								done = true;
								break;
							}
							
						}
						if(!done){ //hang up after 2 seconds
							Thread.sleep(2000);
						}
						time = System.currentTimeMillis()/1000;
						//get response of BYE-packet
						while(!done){
							bye(sp, invitePacket);
							bufTmp = new byte[1024];
							generalPacket = new DatagramPacket(bufTmp, bufTmp.length); 
							ds.receive(generalPacket);
							spErrorChecker.parse(new String(bufTmp)); //parse SIP data
							debug("CALLID: "+spErrorChecker.callId + "; TYPE :" +spErrorChecker.type.split(" ")[0].trim());
							//if we get an OK for our bye, the call is over
							if(spErrorChecker.callId.trim().equals(sp.callId.trim()) && spErrorChecker.type.trim().equals("SIP/2.0 200 OK")){
								done = true;
								callTags.remove(sp.callId);
								callBranch.remove(sp.callId);
								break;
							//if we get a CANCEL packet (the caller had already hang up) then OK it and terminate gracefully
							}else if(spErrorChecker.callId.trim().equals(sp.callId.trim()) && spErrorChecker.type.split(" ")[0].trim().equals("CANCEL")){
								sendOk(sp, "", invitePacket, "CANCEL");
								terminate(sp, invitePacket);
								done = true;
								callTags.remove(sp.callId);
								callBranch.remove(sp.callId);
								break;
							//if we get an INVITE during this call, send BUSY
							}else if(spErrorChecker.callId.trim().equals(sp.callId.trim()) && spErrorChecker.type.split(" ")[0].trim().equals("INVITE")){
								busy(spErrorChecker, generalPacket);
							}else if(System.currentTimeMillis()/1000 - time > 5){
								done = true;
								break;
							}
						}
					}catch(Exception e){
						//should not happen
						error(e, false);
					}
				}else{ //communication did not start with INVITE packet send Bad Request
					sendErr(sp, invitePacket, "400 Bad Request");
				}

			}catch(IOException e){
				error("when receiving: " + e.toString(), false);
			}
		}
	}

	//Sends 100 Trying
	private void trying(SIPparser sp, DatagramPacket p) {

		String msg = "SIP/2.0 100 Trying\r\n";
		msg += "Via: SIP/2.0/UDP " + sp.viaHost + ";rport=" + p.getPort() + ";received=" + p.getAddress().toString().substring(1) + ";" + sp.viaBranch + "\r\n";
		msg += "Content-Length: 0\r\n";
		msg += "Call-ID: " + sp.callId + "\r\n";
		msg += "CSeq: " + sp.cSeq +"\r\n";
		msg += "From: " + sp.from + "\r\n";
		msg += "Server: Owltastic\r\n";
		msg += "To: \"" + properties.getProperty("sip_user").trim() + "\"" + sp.to + ";tag=" + callTags.get(sp.callId) + "\r\n";
		//msg += "Contact: <gushed@kth.se>\r\n";
		msg += "\r\n"; //end header

		byte[] buf = msg.getBytes();
		DatagramPacket sendPacket = new DatagramPacket(buf, buf.length);

		sendPacket.setPort(p.getPort()); //always stay on the port side.
		sendPacket.setAddress(sp.fromAddr); //address to the receiver which is in the from fields

		try{
			ds.send(sendPacket);
		}catch(Exception e){
			error(e, false); 
		}

	}

	//Sends Bye
	private void bye(SIPparser sp, DatagramPacket p) {
		
		int cSeq = 1;
		try{
			cSeq += Integer.parseInt(sp.cSeq.split(" ")[0]);			
		}catch(NumberFormatException e){
			error(e,false);
			sendErr(sp, p, "400 Bad Request");
		}
		String msg = "BYE " + sp.to.replace("<", "").replace(">", "") + " SIP/2.0\r\n";
		msg += "Via: SIP/2.0/UDP " + sp.viaHost + ";rport;branch=" + callBranch.get(sp.callId) + "\r\n";
		msg += "Content-Length: 0\r\n";
		msg += "Call-ID: " + sp.callId + "\r\n";
		msg += "CSeq: " + cSeq + " BYE\r\n";
		msg += "From: \"" + properties.getProperty("sip_user").trim() + "\"" + sp.to + ";tag=" + callTags.get(sp.callId) + "\r\n";
		msg += "Max-Forwards: 70\r\n";
		msg += "To: "+ sp.from + "\r\n";
		msg += "User-Agent: OwlInaBowl-corporation Inc.\r\n";
		//msg += "Contact: <gushed@kth.se>\r\n";
		msg += "\r\n"; //end header

		byte[] buf = msg.getBytes();
		DatagramPacket sendPacket = new DatagramPacket(buf, buf.length);

		sendPacket.setPort(p.getPort()); //always stay on the port side.
		sendPacket.setAddress(sp.fromAddr); //address to the receiver which is in the from fields

		try{
			ds.send(sendPacket);
		}catch(Exception e){
			error(e, false); 
		}

	}
	
	//Sends 486 Busy Here
	private void busy(SIPparser sp, DatagramPacket p) {

		String msg = "SIP/2.0 486 Busy Here\r\n";
		msg += "Via: SIP/2.0/UDP " + sp.viaHost + ";rport;" + sp.viaBranch + "\r\n";
		msg += "Content-Length: 0\r\n";
		msg += "Call-ID: " + sp.callId + "\r\n";
		msg += "CSeq: " + sp.cSeq +"\r\n";
		msg += "From: "+ sp.from + "\r\n";
		msg += "Max-Forwards: 70\r\n";
		msg += "To: \"" + properties.getProperty("sip_user").trim() + "\"" + sp.to + "\r\n";
		msg += "User-Agent: OwlInaBowl-corporation Inc.\r\n";
		//msg += "Contact: <gushed@kth.se>\r\n";
		msg += "\r\n"; //end header
		
		System.out.println("====MSG: "+msg);

		byte[] buf = msg.getBytes();
		DatagramPacket sendPacket = new DatagramPacket(buf, buf.length);

		sendPacket.setPort(p.getPort()); //always stay on the port side.
		sendPacket.setAddress(sp.fromAddr); //address to the receiver which is in the from fields
		
		

		try{
			ds.send(sendPacket);
		}catch(Exception e){
			error(e, false); 
		}

	}
	
	//Sends CANCEL
	private void cancel(SIPparser sp, DatagramPacket p) {

		String msg = "CANCEL " + sp.to.replace("<", "").replace(">", "").replaceAll("\".+\"", "") + " SIP/2.0\r\n";
		msg += "Via: SIP/2.0/UDP " + sp.viaHost + ";rport;" + sp.viaBranch + "\r\n";
		msg += "Content-Length: 0\r\n";
		msg += "Call-ID: " + sp.callId + "\r\n";
		msg += "CSeq: 1 CANCEL\r\n";
		msg += "From: \"" + properties.getProperty("sip_user").trim() + "\"" + sp.to + "\r\n";
		msg += "Max-Forwards: 70\r\n";
		msg += "To: "+ sp.from + "\r\n";
		msg += "User-Agent: OwlInaBowl-corporation Inc.\r\n";
		//msg += "Contact: <gushed@kth.se>\r\n";
		msg += "\r\n"; //end header

		byte[] buf = msg.getBytes();
		DatagramPacket sendPacket = new DatagramPacket(buf, buf.length);

		sendPacket.setPort(p.getPort()); //always stay on the port side.
		sendPacket.setAddress(sp.fromAddr); //address to the receiver which is in the from fields
		
		

		try{
			ds.send(sendPacket);
		}catch(Exception e){
			error(e, false); 
		}

	}

	//Sends Ringing
	private void ringing(SIPparser sp, DatagramPacket p) {

		String msg = "SIP/2.0 180 Ringing\r\n";
		msg += "Via: SIP/2.0/UDP " + sp.viaHost + ";rport=" + p.getPort() + ";received=" + p.getAddress().toString().substring(1) + ";" + sp.viaBranch + "\r\n";
		msg += "Content-Length: 0\r\n";
		msg += "Contact: " + sp.to + "\r\n";
		msg += "Call-ID: " + sp.callId + "\r\n";
		msg += "CSeq: " + sp.cSeq +"\r\n";
		msg += "From: " + sp.from + "\r\n";
		msg += "Server: Owltastic\r\n";
		msg += "To: \"" + properties.getProperty("sip_user").trim() + "\"" + sp.to + ";tag=" + callTags.get(sp.callId) + "\r\n";
		msg += "\r\n"; //end header

		byte[] buf = msg.getBytes();
		DatagramPacket sendPacket = new DatagramPacket(buf, buf.length);

		sendPacket.setPort(p.getPort()); //always stay on the port side.
		sendPacket.setAddress(sp.fromAddr); //address to the receiver which is in the from fields

		try{
			ds.send(sendPacket);
		}catch(Exception e){
			error(e, false); 
		}

	}
	
	//send general error packet, the type specified with the string @error
	private void sendErr(SIPparser sp, DatagramPacket p, String error) {

		String msg = "SIP/2.0 "+error+"\r\n";
		msg += "Via: SIP/2.0/UDP " + sp.viaHost + ";rport=" + p.getPort() + ";received=" + p.getAddress().toString().substring(1) + ";" + sp.viaBranch + "\r\n";
		msg += "Content-Length: 0\r\n";
		msg += "Contact: " + sp.to + "\r\n";
		msg += "Call-ID: " + sp.callId + "\r\n";
		msg += "CSeq: " + sp.cSeq +"\r\n";
		msg += "From: " + sp.from + "\r\n";
		msg += "Server: Owltastic\r\n";
		msg += "To: \"" + properties.getProperty("sip_user").trim() + "\"" + sp.to + ";tag=" + callTags.get(sp.callId) + "\r\n";
		msg += "\r\n"; //end header

		byte[] buf = msg.getBytes();
		DatagramPacket sendPacket = new DatagramPacket(buf, buf.length);

		sendPacket.setPort(p.getPort()); //always stay on the port side.
		sendPacket.setAddress(sp.fromAddr); //address to the receiver which is in the from fields

		try{
			ds.send(sendPacket);
		}catch(Exception e){
			error(e, false); 
		}

	}

	/* 
	 * Send OK packet to address specified in @sp using @port
	 * */
	private void sendOk(SIPparser sp, String rawsdpd, DatagramPacket p, String type) {

		SDPparser sdpp = new SDPparser();
		if((rawsdpd = sdpp.parse(rawsdpd)).equals("ERROR")){
			sendErr(sp, p, "400 Bad Request");
		}
		int cSeq = 1;
		try{
			cSeq += Integer.parseInt(sp.cSeq.split(" ")[0]);			
		}catch(NumberFormatException e){
			error(e,false);
			sendErr(sp, p, "400 Bad Request");
		}
		
		String msg = "SIP/2.0 200 OK\r\n";
		msg += "Via: SIP/2.0/UDP " + sp.viaHost + ";rport=" + p.getPort() + ";received=" + p.getAddress().toString().substring(1) + ";" + sp.viaBranch + "\r\n";
		msg += "From: " + sp.from + "\r\n";
		msg += "To: \"" + properties.getProperty("sip_user").trim() + "\"" + sp.to + ";tag=" + callTags.get(sp.callId) + "\r\n";
		msg += "Call-ID: " + sp.callId + "\r\n";
		msg += "CSeq: " + cSeq + " " + type + "\r\n";
		msg += "Contact: " + sp.to + "\r\n";
		msg += "Server: Owltastic\r\n";
		msg += "Content-Type: application/sdp\r\n";
		msg += "Content-Length: " + rawsdpd.length() +"\r\n";
		msg += "\r\n"; //end header
		msg += rawsdpd;


		byte[] buf = msg.getBytes();
		DatagramPacket sendPacket = new DatagramPacket(buf, buf.length);

		sendPacket.setPort(p.getPort()); //always stay on the port side.
		sendPacket.setAddress(sp.fromAddr); //address to the receiver which is in the from fields

		try{
			ds.send(sendPacket);
		}catch(Exception e){
			error(e, false); 
		}
	}
	
	//Send 487 Request Terminated
	private void terminate(SIPparser sp, DatagramPacket p) {

		String msg = "SIP/2.0 487 Request Terminated\r\n";
		msg += "Via: SIP/2.0/UDP " + sp.viaHost + ";rport=" + p.getPort() + ";received=" + p.getAddress().toString().substring(1) + ";" + sp.viaBranch + "\r\n";
		msg += "From: " + sp.from + "\r\n";
		msg += "To: \"" + properties.getProperty("sip_user").trim() + "\"" + sp.to + ";tag=" + callTags.get(sp.callId) + "\r\n";
		msg += "Call-ID: " + sp.callId + "\r\n";
		msg += "CSeq: " + sp.cSeq + "\r\n";
		msg += "Contact: " + sp.to + "\r\n";
		msg += "Server: Owltastic\r\n";
		msg += "Content-Length: 0\r\n";
		msg += "\r\n"; //end header


		byte[] buf = msg.getBytes();
		DatagramPacket sendPacket = new DatagramPacket(buf, buf.length);

		sendPacket.setPort(p.getPort()); //always stay on the port side.
		sendPacket.setAddress(sp.fromAddr); //address to the receiver which is in the from fields

		try{
			ds.send(sendPacket);
		}catch(IOException e){
			error(e, false); 
		}
	}

	private void error(Object msg, boolean die) {
		System.err.println("== Error: " + msg.toString());
		if(die)
			System.exit(1);
	}

	private void debug(Object msg) {
		System.out.println("== Debug: " + msg.toString());
	}

}


